iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0
AI Engineering

從 LLM 到 Harness: 打造隱私與可信任的繁中進階 OCR Agent系列 第 11 篇

Day 11 - gpt-oss:20b 當 Verifier:為什麼不能自己驗證自己

  • 分享至 

  • xImage
  •  

昨天留下的問題其實很不舒服:沒有答案卷時,誰來說 OCR 對不對?

最省事的答案是讓產生結果的模型再讀一次。也是我不採用的答案。Day 5 已經把這個坑叫出名字:同源盲點。同一個模型若把「阻」讀成「防」,第二輪很可能仍覺得句子通順;它檢查到的是自己的敘事,而不是字形。

規劃裡把 gpt-oss:20b 放在 Verifier 位置,理由很單純:它不是 Google 家的模型,跟負責 OCR 的 gemma-4-26b 不同源。但寫到這裡我停下來問自己一個更笨的問題——我到底有沒有辦法真的把它叫起來?昨天寫完才發現,我對這顆模型的規格其實一無所知,也沒認真算過自己手上的硬體吃不吃得下。今天先把這筆帳算清楚,再決定 Verifier 的介面該長什麼樣。

gpt-oss:20b 到底是什麼規格

查了 Hugging Face 官方模型卡(huggingface.co/openai/gpt-oss-20b):總參數 21B,MoE 架構,每個 token 實際只啟用 3.6B。原生量化格式是 MXFP4(4-bit block-scaled),官方卡上寫的是「run within 16GB of memory」;另外交叉查了 Unsloth 文件、apxml、willitrunai 等幾個獨立來源,都落在同一個量級——MXFP4 權重實際落地大約 14GB,官方建議的最低顯存門檻是 16GB。專家數量與 top-k 路由細節,官方頁面和 arXiv 摘要(2508.10925)都沒寫,我查不到,就不編。

順手換算一下如果不用 MXFP4:FP16 需要 21B × 2 bytes ≈ 42GB,INT8 需要 21B × 1 byte ≈ 21GB。MXFP4 之所以能塞進 16GB,是因為它是專門為這代模型設計的 4-bit 格式,不是我拿 4-bit 粗略估的位元數乘一乘就能算對的東西——這行是我自己按官方數字回推的,不是官方文件裡的原始算式,算是「驗證過官方結論、但沒驗證官方怎麼算出來的」。

三個能跑它的地方,一個都不行

本機:RTX 3050 Laptop,4096 MiB VRAM,實測 nvidia-smi 目前 0 已用。16GB 建議門檻對 4GB 顯卡來說不是「差一點」,是差了一個數量級,GPU 推論這條路直接關掉。

CPU/RAM 混合推論呢?我這台總記憶體 15.6 GiB(wmic 查到 16,782,102,528 bytes,除以 1024³)。Ollama 支援 CPU offload,理論上 14GB 的權重加上作業系統、context、KV cache,會把 15.6 GiB 幾乎吃光——算式上勉強卡在邊緣,但這是「紙上算得過去」,不是「我跑過」。零緩衝空間的配置,我沒有把握它不會在載入到一半時被系統砍掉,也沒有時間預算去賭這件事,所以老實說:我沒試。

GB10(10.0.0.220:8004):128GB 統一記憶體,273 GB/s 頻寬,Day 3 量過。容量上 16GB 對 128GB 是綽綽有餘,看起來像最合理的選項。但兩個現實問題擋在前面:第一,Day 3 已經踩過統一記憶體的坑——host 層的 Ollama 跑 gemma4:26b 時,跟同時在跑的 vLLM container 搶記憶體直接 crash(journalctl 顯示 llama runner segfault),Day 3 的結論是「一次只起一個」。這台機器現在線上跑的就是 gemma-4-26b,OCR 主流程還在用它,我不能因為想測 Verifier 就把生產端點打掉。第二,也是更直接的一點:這台機器的操作權限不在我這裡,我只能打 API,沒辦法登進去換模型或另開一個 instance。

速度會是多少

Day 3 推過一個公式:理論上限 tok/s ≈ 記憶體頻寬 ÷ 每個 token 需要讀取的權重量。gpt-oss:20b 每個 token 只動用 3.6B 活躍參數,MXFP4 大約 0.5 byte/參數:

活躍權重 = 3.6B × 0.5 byte ≈ 1.8 GB
理論上限(GB10)= 273 GB/s ÷ 1.8 GB ≈ 152 tok/s

先把介面訂出來

跑不動歸跑不動,Verifier 這個角色要接什麼、吐什麼,可以先訂死。訂這份契約用得上 Day 5 的教訓:Verifier 不該是「再問模型一次通不通順」,它要回報的是具體疑點,而且要老實承認自己也會不確定。

寫進了 D:\iron-people\harness\verifier_protocol.py,用 Pydantic v2 定義四個東西:

class VerifierVerdict(str, Enum):
    PASS = "PASS"
    FLAG = "FLAG"  # 有疑點但不確定,需要人工或第三方複核
    FAIL = "FAIL"

class VerifierIssue(BaseModel):
    location: str
    original_text: str
    concern: str
    confidence: float = Field(..., ge=0.0, le=1.0)

VerifierVerdict 刻意做成三態而不是二元的 PASS/FAIL。二元判定逼著模型在不確定的時候硬選一邊,FLAG 給它一個「我看到怪但不確定」的出口——這件事本身也是為了避免同源盲點的另一種變形:如果 Verifier 因為輸出格式所迫必須表態,它在不確定時很可能傾向選 PASS(阻力最小的答案),那就跟沒驗證一樣。VerifierIssue.confidence 加了 ge=0.0, le=1.0 的範圍驗證,這條我實際測過會擋下超出範圍的值:

VerifierIssue(location="test", original_text="test", concern="test", confidence=1.5)
→ pydantic_core._pydantic_core.ValidationError:
  Input should be less than or equal to 1

VerifierResponse.model_id 這個欄位是專門留給稽核用的:以後不管接的是 gpt-oss:20b 還是別的模型,這裡都要老實記下「這次判定實際用了哪個模型」,避免有人(包括我自己)不小心把 Verifier 換成跟主模型同源的東西卻沒發現。目前它的值寫死成 "gpt-oss:20b (design-only, not yet wired)"——這個字串本身就是在承認「這只是設計,還沒真的接上」,demo 資料不能偽裝成真實推論結果。

跑一下 __main__ 區塊的 demo(純本地物件序列化,沒有任何網路呼叫):

{
  "verdict": "FLAG",
  "issues": [
    {
      "location": "p.3 表格區塊 2 / 金額欄",
      "original_text": "NT$1,2O0",
      "concern": "數字中疑似混入字母 O 取代數字 0,金額判讀可能有誤",
      "confidence": 0.82
    }
  ],
  "model_id": "gpt-oss:20b (design-only, not yet wired)",
  "latency_ms": 0.0
}

設計方向

Verifier 只回報判定與疑點,原始 OCR 輸出保持不動;判定用三態(PASS/FLAG/FAIL)而不是二元,讓模型有老實說「不確定」的空間;model_id 欄位強制記錄實際用的模型,防止同源盲點在稽核層被悄悄繞過。


上一篇
Day 10 - 純文字區與表格區實測:50 tok/s 與 7 tok/s 的取捨
下一篇
Day 12 - 隱私的第一道防線:模型供應鏈審查與備選評測
系列文
從 LLM 到 Harness: 打造隱私與可信任的繁中進階 OCR Agent 共 12 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言